fix(proxy): pass-through multipart uploads and Bedrock JSON body#25464
fix(proxy): pass-through multipart uploads and Bedrock JSON body#25464yuneng-berri merged 5 commits intomainfrom
Conversation
- Route multipart forwarding on forward_multipart instead of empty _parsed_body so litellm_logging_obj no longer forces json= for file uploads. - Remove custom_body from pass-through endpoint signatures; FastAPI treated it as a JSON body and rejected multipart before the handler ran. Bedrock passes JSON via request.state (LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY). - Use build_request + send(stream=True) for streaming multipart; httpx 0.28 AsyncClient.request does not accept stream=. - Add regression test for non-empty _parsed_body multipart path; update Bedrock custom-body test and query-params test for forward_multipart. Made-with: Cursor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@greptile review |
Greptile SummaryThis PR fixes three root causes behind broken multipart/form-data pass-through and Bedrock JSON forwarding: (1) removing Confidence Score: 5/5PR is safe to merge — three clearly-scoped root-cause fixes with corresponding mock tests and no new P0/P1 issues. All remaining findings are P2 or lower. The logic for No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/types/passthrough_endpoints/pass_through_endpoints.py | Adds LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY constant to the types module, giving both proxy files a single import source and avoiding circular imports. |
| litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py | Replaces custom_body=data kwarg on endpoint_func with setattr(request.state, LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY, data) before calling the endpoint; imports the constant correctly from the types module. |
| litellm/proxy/pass_through_endpoints/pass_through_endpoints.py | Core logic changes: adds explicit forward_multipart flag to non_streaming_http_request_handler, adds stream param to make_multipart_http_request (uses build_request+send path), handles streaming multipart in pass_through_request, and reads/cleans up request.state key in create_pass_through_route. |
| tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py | Adds regression test for multipart with non-empty _parsed_body, updates test_create_pass_through_route_custom_body_url_target to use request.state injection, and adds SimpleNamespace() state to tests that previously lacked it. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[bedrock_proxy_route] -->|setattr request.state| B[endpoint_func via create_pass_through_route]
B -->|reads state_custom_body| C{state_custom_body?}
C -->|Yes - dict| D[final_custom_body = state_custom_body]
C -->|No| E{custom_body_data from request?}
E -->|Yes| F[final_custom_body = custom_body_data]
E -->|No| G[final_custom_body = None]
D & F & G --> H[pass_through_request custom_body=final_custom_body]
H --> I{is_multipart? = is_multipart_request AND NOT custom_body}
I -->|True| J{stream?}
I -->|False| K{stream?}
J -->|Yes| L[make_multipart_http_request build_request + send stream=True]
J -->|No| M[non_streaming_http_request_handler forward_multipart=True]
K -->|Yes| N[build_request + send stream=True]
K -->|No| O[non_streaming_http_request_handler forward_multipart=False json=_parsed_body]
B -->|finally: delattr state key| P[cleanup request.state]
Reviews (5): Last reviewed commit: "refactor: define pass-through custom bod..." | Re-trigger Greptile
| if stream: | ||
| req = async_client.build_request( | ||
| request.method, | ||
| url, | ||
| headers=headers_copy, | ||
| params=requested_query_params, | ||
| files=files, | ||
| data=form_data_dict, | ||
| ) | ||
| return await async_client.send(req, stream=True) |
There was a problem hiding this comment.
Missing test for streaming multipart path
The stream=True branch in make_multipart_http_request (build_request + send(stream=True)) is new code introduced by this PR, but none of the existing unit tests exercise it. Both test_make_multipart_http_request and test_make_multipart_http_request_removes_content_type_header only mock async_client.request and never pass stream=True, so the build_request/send path is entirely untested. A dedicated test should mock async_client.build_request and async_client.send to verify headers, files, and the stream=True argument are forwarded correctly.
Restores Windows-style line endings to match main/origin main for this file, removing the full-file noise diff from an accidental LF-only normalization. Made-with: Cursor
…import' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
| is_streaming_request=is_streaming_request, | ||
| _forward_headers=True, | ||
| ) # dynamically construct pass-through endpoint based on incoming path | ||
| setattr(request.state, LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY, data) |
There was a problem hiding this comment.
NameError — LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY is not in scope
LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY is defined in pass_through_endpoints.py but is not imported into this module. The top-level import block (lines 38-43) brings in HttpPassThroughEndpointHelpers, create_pass_through_route, etc., but not this constant. _get_litellm_pass_through_custom_body_state_key() was added to provide a lazy import, but it is never called here — the bare name is referenced instead. Every call to bedrock_proxy_route will raise NameError: name 'LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY' is not defined.
Fix: add the constant to the existing top-level import (no circular-import concern — llm_passthrough_endpoints already imports from pass_through_endpoints at module level):
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
HttpPassThroughEndpointHelpers,
LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY,
create_pass_through_route,
create_websocket_passthrough_route,
websocket_passthrough_request,
)The _get_litellm_pass_through_custom_body_state_key() helper can then be removed as it becomes dead code.
…ssthrough Fixes NameError when bedrock_proxy_route sets custom body on request.state. Remove unused lazy-loader helper. Made-with: Cursor
|
@greptile review again with new commit that resolves p0 |
Avoid module-level cyclic import between llm_passthrough_endpoints and pass_through_endpoints; CodeQL and partial init order no longer risk undefined LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY. Made-with: Cursor
|
@greptile review with new commit that remove cyclic import |
Relevant issues
fixes #23338
Pass-through routes now forward multipart/form-data (e.g. file uploads) correctly. Programmatic Bedrock-style callers still supply a pre-built JSON body via request.state instead of a route parameter. Streaming multipart uses build_request + send(stream=True) for httpx 0.28. Adds/updates unit tests for multipart forwarding and forward_multipart.
Cause
custom_body: Optional[dict] on the pass-through handler — FastAPI treated it as the HTTP JSON body, so multipart was validated as JSON and failed before the handler ran (often surfacing as RequestValidationError / UTF-8 errors on binary payloads).
not _parsed_body gating multipart — After injecting litellm_logging_obj, _parsed_body was never empty, so the code took json=_parsed_body instead of rebuilding multipart.
AsyncClient.request(..., stream=...) — In httpx 0.28, request() does not accept stream; streaming must use send(..., stream=True) after build_request.
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
tests/test_litellm/directory, Adding at least 1 test is a hard requirement - see detailsmake test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewDelays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
CI (LiteLLM team)
Branch creation CI run
Link:
CI run for the last commit
Link:
Merge / cherry-pick CI run
Links:
Type
🐛 Bug Fix
✅ Test
Changes
Fix
Remove body-parameter custom_body from pass-through handlers; read optional programmatic JSON from request.state[LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY] and clear it in finally. Bedrock sets that attribute before calling the handler.
Pass forward_multipart=is_multipart into non_streaming_http_request_handler and branch on that (with is_multipart still meaning “multipart request and no JSON envelope custom_body”).
In make_multipart_http_request, if stream is true, use build_request + send(stream=True); otherwise keep request(...) without stream.
Add/update tests for the above paths.